﻿#region

using System;
using System.Threading;
using System.Windows.Browser;
using System.Windows.Media;

#endregion

namespace Radovici.Ctrls
{
    public class YouTubePlayer : VideoBlock
    {
        #region PlayerState enum

        public enum PlayerState
        {
            NotLoaded = -1024,
            Unstarted = -1,
            Ended = 0,
            Playing = 1,
            Paused = 2,
            Buffering = 3,
            Cued = 5
        }

        #endregion

        private readonly SynchronizationContext _context;
        private readonly Timer _tmr;
        private PlayerState _curState = PlayerState.NotLoaded;

        public YouTubePlayer()
        {
            _context = SynchronizationContext.Current;
            //HtmlPage.Window.AttachEvent("onStateChange", onStateChanged);
            //HtmlPage.Window.AttachEvent("onError", onError);
            _tmr = new Timer(onTimerCallback, null, 0, 1000);
        }

        //private void onStateChanged(object state, HtmlEventArgs e)
        //{
        //    Debug.WriteLine(state);
        //}

        //private void onError(object state, HtmlEventArgs e)
        //{
        //    Debug.WriteLine(state);
        //}

        public bool RelatedEnabled { get; set; }
        public bool AutoPlay { get; set; }
        public bool Loop { get; set; }
        public bool KeyboardEnabled { get; set; }
        public bool MenuEnabled { get; set; }
        public bool CustomBorderEnabled { get; set; }
        public Color CustomBorderColor { get; set; }
        public Color CustomBorderControlBarColor { get; set; }
        public double StartTime { get; set; }
        public bool FullScreenEnabled { get; set; }
        public bool SearchEnabled { get; set; }
        public bool ShowInfo { get; set; }
        public bool ShowAnnotations { get; set; }
        public bool ShowCaptions { get; set; }

        private string Query
        {
            get
            {
                string query = string.Empty;
                query += RelatedEnabled ? "&rel=1" : "&rel=0";
                query += AutoPlay ? "&autoplay=1" : "&autoplay=0";
                query += Loop ? "&loop=1" : "&loop=0";
                query += KeyboardEnabled ? "&disablekb=0" : "&disablekb=1";
                query += MenuEnabled ? "&egm=1" : "&egm=0";
                query += CustomBorderEnabled
                             ? "&border=1&color1=" + CustomBorderColor + "&color1=" + CustomBorderControlBarColor
                             : "&border=0";
                query += "&start=" + StartTime;
                query += FullScreenEnabled ? "&fs=1" : "&fs=0";
                query += SearchEnabled ? "&showsearch=1" : "&showsearch=0";
                query += ShowInfo ? "&showinfo=1" : "&showinfo=0";
                query += ShowAnnotations ? "&iv_load_policy=1" : "&iv_load_policy=3";
                query += ShowCaptions ? "&cc_load_policy=1" : "&cc_load_policy=0";
                return query;
            }
        }

        protected override string VideoHtml
        {
            get
            {
                Stop(); //Stop before attempting to load video by default.
                return HtmlHelper.GetVideoHtml(VideoUrl + "&enablejsapi=1" + Query, VideoId);
            }
        }

        /// <summary>
        /// Returns the state of the player. Possible values are unstarted (-1), ended (0), playing (1), paused (2), buffering (3), video cued (5).
        /// </summary>
        public PlayerState CurrentState
        {
            get { return _curState; }
            private set
            {
                if (_curState != value)
                {
                    if (StateChanged != null)
                    {
                        StateChanged(this, new StateChangedEventArgs(_curState, value));
                    }
                }
                _curState = value;
            }
        }

        /// <summary>
        /// Declarative way to play or pause the video.
        /// </summary>
        public bool IsPlaying
        {
            get { return CurrentState == PlayerState.Playing; }
            set
            {
                if (value)
                {
                    Play();
                }
                else
                {
                    Pause();
                }
            }
        }

        /// <summary>
        /// Returns true if the player is muted, false if not.
        /// </summary>
        public bool IsMuted
        {
            get { return Convert.ToBoolean(Eval("isMuted()")); }
            set
            {
                if (value)
                {
                    Mute();
                }
                else
                {
                    UnMute();
                }
            }
        }

        /// <summary>
        /// Gets or sets the volume. 
        /// Accepts an integer between 0-100.
        /// Returns the player's current volume, an integer between 0-100.
        /// Note that getVolume() will return the volume even if the player is muted.
        /// </summary>
        public double Volume
        {
            get { return Convert.ToDouble(Eval("getVolume()")); }
            set { Eval(string.Format("setVolume({0})", value)); }
        }

        /// <summary>
        /// Gets or sets the current time in seconds of the current video.
        /// </summary>
        public double CurrentTime
        {
            get { return Convert.ToDouble(Eval("getCurrentTime()")); }
            set { Seek(value, true); }
        }

        /// <summary>
        /// Returns the duration in seconds of the currently playing video.
        /// Note that getDuration() will return 0 until the video's metadata is loaded, which normally happens just after the video starts playing.
        /// </summary>
        public double Duration
        {
            get { return Convert.ToDouble(Eval("getDuration()")); }
        }

        public event EventHandler<StateChangedEventArgs> StateChanged;

        /// <summary>
        /// Plays the currently cued/loaded video.
        /// </summary>
        public void Play()
        {
            Eval("playVideo()");
        }

        /// <summary>
        /// Pauses the currently playing video.
        /// </summary>
        public void Pause()
        {
            Eval("pauseVideo()");
        }

        /// <summary>
        /// Stops the current video.
        /// This also closes the NetStream object and cancels the loading of the video.
        /// Once stopVideo() is called, a video cannot be resumed without reloading the player or loading a new video (chromeless player only).
        /// When calling stopVideo(), the player will broadcast an end event (0).
        /// </summary>
        public void Stop()
        {
            Eval("stopVideo()");
        }

        /// <summary>
        /// Clears the video display. Useful if you want to clear the video remnant after calling stopVideo().
        /// </summary>
        public void Clear()
        {
            Eval("clearVideo()");
        }

        /// <summary>
        /// Mutes the player.
        /// </summary>
        private void Mute()
        {
            Eval("mute()");
        }

        /// <summary>
        /// Unmutes the player.
        /// </summary>
        private void UnMute()
        {
            Eval("unMute()");
        }

        /// <summary>
        /// Seeks to the specified time of the video in seconds.
        /// Note that seekTo() will look for the closest keyframe before the seconds specified.
        /// This means that sometimes the play head may seek to just before the requested time, usually no more than ~2 seconds.
        /// </summary>
        /// <param name="seconds"></param>
        /// <param name="allowSeekAhead">The allowSeekAhead determines whether or not the player will make a new request to the server if seconds is beyond the currently loaded video data.</param>
        public void Seek(double seconds, bool allowSeekAhead)
        {
            Eval(string.Format("seekTo({0},{1})", seconds, allowSeekAhead));
        }

        /// <summary>
        /// Returns the number of bytes loaded for the current video.
        /// </summary>
        public double BytesLoaded()
        {
            return Convert.ToDouble(Eval("clearVideo()"));
        }

        /// <summary>
        /// Returns the size in bytes of the currently loaded/playing video.
        /// </summary>
        public double BytesTotal()
        {
            return Convert.ToDouble(Eval("getVideoBytesTotal()"));
        }

        /// <summary>
        /// Returns the number of bytes the video file started loading from.
        /// Example scenario: the user seeks ahead to a point that hasn't loaded yet, 
        /// and the player makes a new request to play a segment of the video that hasn't loaded yet.
        /// </summary>
        public double StartBytes()
        {
            return Convert.ToDouble(Eval("getVideoStartBytes()"));
        }

        #region Private methods

        private void onTimerCallback(object state)
        {
            if (_tmr != null) //HACK: If HtmlBlock was disposed
            {
                lock (_tmr)
                {
                    CurrentState = GetPlayerState();
                }
            }
        }

        private PlayerState GetPlayerState()
        {
            object tmp = Eval("getPlayerState()");
            if (tmp != null && tmp.GetType().IsAssignableFrom(typeof (double)))
            {
                return (PlayerState) Convert.ToInt32(tmp);
            }
            return PlayerState.NotLoaded;
        }

        private object Eval(string function)
        {
            var dEval = new SendOrPostCallback(Eval);
            var state = new EvalState {Function = function};
            _context.Send(dEval, state);
            return state.Output;
        }

        private void Eval(object state)
        {
            var evalState = (EvalState) state;
            object tmp =
                HtmlPage.Window.Eval(
                    string.Format(
                        @"(function()
{{
    try
    {{
        ytplayer = document.getElementById('{1}'); 
        if (ytplayer)
        {{
            return ytplayer.{0};
        }}; 
    }}
    catch (ex)
    {{
        return ex;
    }}
}})()",
                        evalState.Function, VideoId));
            evalState.Output = tmp;
        }

        private class EvalState
        {
            public string Function { get; set; }
            public object Output { get; set; }
        }

        #endregion

        #region Nested type: StateChangedEventArgs

        public class StateChangedEventArgs : EventArgs
        {
            public StateChangedEventArgs(PlayerState oldSta, PlayerState newSta)
            {
                OldState = oldSta;
                NewState = newSta;
            }

            public PlayerState OldState { get; private set; }
            public PlayerState NewState { get; private set; }
        }

        #endregion
    }
}